# 该类包含以下功能
#   ui自动化数据初始化（忽略无效证书的提示）
#   飞书认证状态判断
#   飞书重新认证
#   开启/停止 playwright录制
#   数据清理

from playwright.sync_api import sync_playwright
from playwright.sync_api import expect
import time
from datetime import datetime
import os
from Comm.logs import Logs
import base64
from datetime import datetime
from Comm import ExeclTool as ext

class ErpPage:
    # 类的初始化
    def __init__(self,target_url):
        self.p = None
        self.browser = None
        self.page = None
        self.context = None
        self.target_url = target_url    # 访问路径
        self.fs_book_url = "accounts.feishu.cn"     # 飞书域名
        self.storage_state_path = "../auth_state.json"  # 认证状态存储路径
        # self.is_end = False     # 是否结束测试
        self.logger = Logs().get_logger()       # 日志记录器
        self.is_auth_state = False      # 飞书认证状态
        self.response_handler = None  # # 保存响应监听器引用

    # python的上下文管理 进入with时调用enter，负责资源的初始化
    def __enter__(self):
        self.page_start()
        return self

    # 退出with时，清理占用的资源
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.page_end()

    # 开始
    def page_start(self):
        try:  # 异常捕获 尝试执行代码
            self.p = sync_playwright().start()
            self.browser = self.p.chromium.launch(
                headless=False,
                # 访问的页面增加了安全访问的提示，如果直接允许会报错，因为程序无法到达目标页面会一直停留在安全警告页面，
                args=["--ignore-certificate-errors"]    # 在启动浏览器时，增加忽略无效证书，也可以在上下文管理中增加
            )

            # 判断存储飞书认证状态的文件路径是否存在 以及认证状态是否过期
            self.is_fs_auth_status()

            # # -------执行测试
            # if self.is_auth_state:  # 如果认证成功
            #     self.first_feedback_pop()  # 调用提交反馈弹窗是否出现的方法

            return self     # 返回实例self，类的外部可以直接访问私有属性和方法

        except Exception as e:  # 出现异常
            self.logger.error(f"[ErpPage.page_start]执行异常！异常内容为：[{e}]")

        # finally:
        #     self.logger.info(f"测试结束！")
        #     self.page_end()  # 调用结束的方法，关闭资源

    # 判断飞书认证状态文件是否存在、认证状态是否过期
    def is_fs_auth_status(self ):
        if os.path.exists(self.storage_state_path):  # 如果存在
            self.context = self.browser.new_context(
                storage_state=self.storage_state_path)  # 创建浏览器上下文时，使用存储的认证数据
            # 增加忽略无效证书：ignore_https_errors=True
            self.page = self.context.new_page()
            self.page.goto(self.target_url)  # 访问页面

            # 判断当前页面是否还在飞书域名内
            is_fs_url = self.is_fs_page_url()  # 是的话代表认证过期了
            if is_fs_url:  # 是，认证过期了，需要重新认证
                self.logger.info(f"认证状态已过期！！重新开始认证...请在15s内完成飞书认证...")
                os.remove(self.storage_state_path)  # 删除旧的文件数据
                self.fs_auth()  # 重新认证
            else:  # 否，可以直接开始测试
                self.logger.info(f"认证状态未过期，开始测试！当前页面路径为[{self.page.url}]")
                self.is_auth_state = True
        else:
            self.logger.info("认证状态存储的文件路径不存在！重新开始认证...请在15s内完成飞书认证...")
            self.context = self.browser.new_context()
            self.page = self.context.new_page()
            self.page.goto(self.target_url)  # 访问页面
            self.fs_auth()  # 重新认证

    # 判断当前页面路径是否在飞书域名内
    def is_fs_page_url(self):
        try:
            if self.fs_book_url in self.page.url:return True
            else:return False

        except Exception as e:
            self.logger.error(f"[ErpPage.is_fs_page_url]执行异常！异常内容为：[{e}]")

    # 飞书认证流程
    def fs_auth(self, second=15):
        try:
            for s in range(0,second):
                is_fs_url = self.is_fs_page_url()  # 判断当前页面是否还在飞书域名内
                self.page.wait_for_timeout(1000)
                if s == second-1 and is_fs_url == True:
                    self.logger.info("等待时间内，用户未完成认证，退出测试！")
                    break

                if is_fs_url == False:
                    if self.context is not None:
                        self.context.storage_state(path=self.storage_state_path)  # 重新存储飞书认证状态
                        self.logger.info(
                            f"在第{s}秒时，用户认证成功！新的认证状态已经存储到[{self.storage_state_path}]中！当前页面路径为[{self.page.url}]")
                        self.is_auth_state = True

                    else:  # context为空，说明窗口创建失败，结束测试
                        self.logger.info("context为空，退出测试！")

                    break

            # erp_logo = 'ant-pro-top-nav-header'
            # if self.page is not None:
            #     # self.page.wait_for_timeout(15000)  # 等待15s 15s内用户需扫码认证完成
            #     self.page.wait_for_selector('erp_logo',timeout=15000)
            #
            #     is_fs_url = self.is_fs_page_url()  # 判断当前页面是否还在飞书域名内
            #     if is_fs_url:  # 是的话代表用户没有完成认证，退出测试
            #         self.logger.info("等待时间内，用户未完成认证，退出测试！")
            #     else:
            #         if self.context is not None:
            #             self.context.storage_state(path=self.storage_state_path)  # 重新存储飞书认证状态
            #             self.logger.info(
            #                 f"用户认证成功！新的认证状态已经存储到[{self.storage_state_path}]中！当前页面路径为[{self.page.url}]")
            #             self.is_auth_state = True
            #
            #         else:  # context为空，说明窗口创建失败，结束测试
            #             self.logger.info("context为空，退出测试！")

            # else:  # page为空，说明窗口创建失败，结束测试
            #     self.logger.info("page为空，退出测试！")

        except Exception as e:
            self.logger.error(f"[ErpPage.fs_auth]执行异常！异常内容为：[{e}]")

    # 首次访问会弹一个提交反馈的弹窗 如果出现就自动点击结束按钮 好像有问题...判断不准确？
    def first_feedback_pop(self):
        elem_feedback_pop = '.__floater.__floater__open'
        elem_btu_close = '//*[@id="react-joyride-step-0"]/div/div/div[1]/div[2]/button'
        try:
            # self.page.on("domcontentloaded", lambda: print("DOM内容加载完成"))    # 监听dom加载
            # self.page.wait_for_selector(button_feedback_end,timeout=5000)    # 等待元素出现 如果找到元素，则返回元素handle，如果超时还没找到元素，则抛异常，但我不想抛异常
            # if self.page.locator(button_feedback_end).count() > 0:      # 判断不准确，会出现count=0但click成功的场景，走不进这个判断
            # pop = self.page.locator(button_feedback_end)
            pop = self.page.locator(elem_feedback_pop)
            print(pop)
            if pop.is_visible():       # 判断元素是否可见，不会抛异常，返回true/false
                self.logger.info("首次访问出现提交反馈的弹窗！")
                self.page.locator(elem_btu_close).click()
                self.logger.info("用户点击[结束]按钮！")
            else:
                self.logger.info("没有出现提交反馈的弹窗！继续执行测试！")

        except Exception as e:
            self.logger.error(f"[ErpPage.first_feedback_pop]执行异常！异常内容为：[{e}]")

    # 判断元素是否存在，是否可见
    def is_visible(self,element):
        if self.page is not None:
            self.page.locator(element).get_by_text("新增")

    # 计算页面加载时长
    def get_page_load_times(self, target_page, max_wait_time=30):
        # 判断页面路径是否正确

        # 尝试记录页面加载时长
        self.page.wait_for_timeout(1500)  # 等待1s，让页面状态稳定,不然会出现页面还没加载完成就跳转下个页面
        start_timestamp = time.time()  # 开始计时
        try:
            self.page.goto(target_page, wait_until="domcontentloaded")  # 访问目标页面 直到dom加载完成
            # 监听页面加载时长
            self.page.wait_for_load_state("load", timeout=max_wait_time * 2000)

            # 定位列表元素
            elem_table = '.ant-table-body'
            try:
                elem = self.page.locator(elem_table)  # 定位元素
                expect(elem).to_be_attached(
                    timeout=max_wait_time * 1000)  # 判断列表元素在dom结果中是否存在 存在不代表要可见 验证结果当列表在页面上可见但有加载标识遮罩时，判断是比较准确的
                # expect(elem).to_be_visible(timeout=max_wait_time * 1000)    # 如果等列表元素可见的话，判断不太准确，列表元素和加载标识可能同时出现 导致加载标识隐藏太快/还没隐藏就跳到下个页面了
                elem_table_num = elem.count()
                self.page.evaluate(
                    f"console.log('1、列表标志数量是：{elem_table_num}！', new Date().toLocaleString())")

            except Exception as e:
                elem_table_num = 0
                print(f"列表元素定位失败！{e}")

            # 定位加载标识元素
            elem_loading = ".ant-spin-dot.ant-spin-dot-spin"  # 特定元素 加载标识 页面会存在多个加载标识 需要等到列表元素出现的时候再判断加载标识
            if elem_table_num > 0:
                try:
                    elem = self.page.locator(elem_loading)  # 定位元素
                    expect(elem).to_be_hidden(timeout=max_wait_time * 1000)
                    self.page.evaluate(
                        f"console.log('2、加载标志数量是：{elem.count()}！', new Date().toLocaleString())")

                except Exception as e:
                    print(f"加载标识元素定位失败！{e}")

            # 等待网络空闲
            self.page.wait_for_load_state("networkidle", timeout=max_wait_time * 1000)

            end_timestamp = time.time()
            load_time = f"{end_timestamp - start_timestamp:.2f}"
            print(f"页面加载时长为：{load_time}")

        except Exception as e:
            end_timestamp = time.time()
            load_time = '页面加载超过' + str(max_wait_time) + 's'
            print(f"页面加载超时{e}")

        start_time = datetime.fromtimestamp(start_timestamp).strftime("%Y-%m-%d %H:%M:%S")
        end_time = datetime.fromtimestamp(end_timestamp).strftime("%Y-%m-%d %H:%M:%S")

        return start_time, end_time, load_time

    # 监听网络请求 收集异常接口信息
    def get_error_response(self,target_page):
        remove_url = ['/skywalking/v3/segments', '/skywalking/browser/errorLogs']  # 剔除这两个接口
        json_type = 'application/json'
        error_code = [404, 405, 500, 504, 502,"502 Bad Gateway"]  # 接口错误状态码
        error_request = []

        # 监听网络请求状态码 常规判断
        def handle_responses(response):  # 服了 请求失败的状态码也是200 没办法通过请求状态码来判断
            try:
                # !! 在监听页面响应时， 得判断页面是否打开 是否被关闭
                if self.page is None:
                    print("页面打开失败，是空对象，监听失败，请检查！！")
                    return
                if self.page.is_closed():
                    print("页面被关闭，监听失败！请检查！")
                    return

                if response.url in "https://erp-fat.hesung.com/api/system/config/check_user_profile_already":
                    status_code = response.status
                    if status_code in error_code:
                        error_request.append({
                            # 'targe_page': target_page,
                            'api_url': response.url,
                            "api_status_code": response.status,
                            "code": status_code,
                            "api_timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())),  # 响应结束时间
                            "msg": "网关错误！"
                        })

                try:
                    content_type = response.headers.get('content-type')
                except Exception as e:
                    print(f"content-type获取失败: {e}")
                    return

                # 响应结果路径没有在指定数组内 并且 响应请求头的content_type是对应的json格式
                if response.url not in remove_url and content_type in json_type:
                    # status_code = response.status
                    response_json = response.json()
                    code = response_json.get('code', 0)
                    if code in error_code:  # 如果接口请求出现异常 收集异常的接口、状态码、错误时间
                        msg = response_json.get('message', '无msg字段')  # 给一个默认值 避免响应结果为空/没有message字段
                        error_request.append({
                            # 'targe_page': target_page,
                            'api_url': response.url,
                            "api_status_code": response.status,
                            "code": code,
                            "api_timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())),  # 响应结束时间
                            "msg": msg
                        })
            except Exception as e:
                print(f"监听异常！{e}")
                return

        # 如果之前有监听器，先移除
        if self.response_handler is not None and self.page is not None:
            try:
                self.page.remove_listener("response", self.response_handler)
            except Exception as e:
                pass

        # 保存新的监听器引用
        self.response_handler = handle_responses

        # 监听网络请求
        self.page.on("response", handle_responses)
        return error_request

    # 开始页面跟踪
    def start_tracing(self):
        try:
            if self.context is not None:
                self.context.tracing.start(snapshots=True, sources=True, screenshots=True)
        except Exception as e:
            self.logger.error(f"[ErpPage.start_tracing]执行异常！playwright开启跟踪失败！失败内容为[{e}]")

    # 结束playwright的页面跟踪，保存录制文件
    def stop_tracing(self,trace_path):
        try:
            if self.context is not None:
                self.context.tracing.stop(path=trace_path)
        except Exception as e:
            self.logger.error(f"[ErpPage.stop_tracing]执行异常！playwright停止跟踪失败！失败内容为[{e}]")

    # 结束 关闭资源
    def page_end(self):
        try:
            if self.browser is not None:  # 如果不为空
                self.browser.close()  # 关闭
                self.browser = None

            if self.p is not None:  # 如果不为空
                self.p.stop()  # 关闭
                self.p = None

            self.context = None
            self.page = None

            self.logger.info(f"测试结束！清理相关资源成功！")

        except Exception as e:
            self.logger.error(f"[ErpPage.page_end]执行异常！异常内容为：[{e}]")


if __name__ == '__main__':
    def text_1():
        # targe_page = "https://erp-dev.hesung.com/eip/po/rapOrderToB/?tabState=2"  # 要访问的页面路径
        # erp_url = "https://erp-fat.hesung.com"
        # erp_url = "https://erp.hesung.com"
        dev = 'https://erp-dev.hesung.com'
        page_1 = dev + '/eip/po/rapOrderToB/?tabState=2'
        page_2 = dev + '/store/rapStoreManage/book?tabState=all'
        ep = ErpPage(dev)
        ep.page_start()
        # 如果认证成功
        if ep.is_auth_state:
            targe_page = "https://erp.hesung.com/eip/po/rapOrderToB?tabState=2"
            # ep.get_page_load_time_1(page_1)
            # ep.get_page_load_time_1(page_2)
            # ep.get_page_load_time_3(page_2)

        ep.page_end()

    # 计算erp页面加载时长练习 使用page.goto()
    def text_2():
        erp = 'https://erp-fat.hesung.com'
        ep = ErpPage(erp)
        ep.page_start()
        ep.first_feedback_pop()
        # 如果认证成功
        if ep.is_auth_state:
            execl_file_path = "D:\python_pj\playwright_test\execl\execl_page_url_test.xlsx"
            sheet_name = 'page_url'
            ex = ext.ExeclTool(execl_file_path, sheet_name)
            ex_len = ex.get_execl_len()
            for r0, r1 in zip(range(ex_len + 1), range(2, ex_len + 2)):  # 读取和写入的索引不一样 我真服了
                # for r0, r1 in zip(range(3), range(2, 6)):
                try:
                    page_url = erp + ex.get_execl_cell(r0, "页面路径")
                    start_time, end_time, loadtime = ep.get_page_load_times(page_url)
                    error_request = ep.get_error_response(page_url)
                    ex.update_execl_cell(r1, 3, start_time)
                    ex.update_execl_cell(r1, 4, end_time)
                    ex.update_execl_cell(r1, 5, loadtime)
                    execl_str = ''
                    if error_request:  # 如果异常接口不为空
                        for i in range(0, len(error_request)):
                            url = error_request[i]["api_url"]
                            code = error_request[i]["code"]
                            msg = error_request[i]["msg"]
                            api_info = f'url:{url} \ncode:{code} \nmsg:{msg}'  # 拼接一下异常信息
                            execl_str += api_info
                            execl_str += '\n'
                        ex.update_execl_cell(r1, 6, execl_str)  # 写入单元格
                        ex.cell_fill_color(r1, 6, 'CD5C5C')  # 填充单元格颜色

                    print(f"第{r1}行数据更新成功！")
                    # ep.page.wait_for_timeout(1000)  # 额外等待1秒
                except Exception as e:
                    print(f"循环出现异常：{e}")
                    ex.update_execl_cell(r1, 5, e)
        ep.page.wait_for_load_state("networkidle", timeout=1000)
        ep.page.wait_for_timeout(1)  # 等待3秒
        ep.page_end()

    text_2()


